Bundler: Webpack이 Vite보다 느린 이유
Webpack 시대에는 몇 초 단위로 기다리는 것이 당연했지만, Vite와 esbuild처럼 ESM(ECMAScript Module) 기반 도구들이 등장하면서 서버가 즉시 뜨고, 수정하면 바로 반영되는 개발 환경이 가능해졌다.
그렇다면 도대체 ESM 기반 도구들은 왜 빠를까?
그리고 Webpack은 왜 느릴까?
또, esbuild는 왜 번들러로 널리 쓰이지 않을까?
Webpack이 느린 이유
Webpack은 태생이 CJS 기반의 번들러이다.
최초 웹팩이 만들어질 당시에는 ESM이 활성화 된 시기가 아니였다. 따라서 내부 Parser, Dependency Graph 분석 방식, 실행 래퍼를 CJS 모듈들 기준으로 설계되었다.
CJS를 전제로 번들링을 하는 웹팩의 단점은 아래와 같다.
초창기 웹팩은 ESM을 기본으로 처리하지 않았기 때문에, Babel은 코드를 추상 구문 트리(AST)로 파싱하여 import 문을 require 함수 호출로 변경합니다. 즉, CJS 기반으로 트랜스파일과 번들링을 진행하면서 아래와 같은 단점이 생겼다.
- 동적 경로를 만드는
require()를 흉내내느라 빌드 시간이 오래걸린다. CJS의require()는 런타임에 동적으로 호출될 수 있어, 웹팩이 빌드 시점에 모든 의존성 관계를 정적으로 파악하는 데 어려움이 있다.require()의 인자로 변수가 들어가는 경우, 웹팩은 런타임을 흉내내면서 가능한 모든 경우를 포함하도록 의존성 그래프를 구성해야 되므로, 이는 빌드 시간을 크게 늘리는 원인이 되었다. - 객체 하나만 내보내는
module.exports때문에 트리쉐이킹이 쉽지 않다. CJS의module.exports는 객체 전체를 내보내는 방식이라, 웹팩이 사용되지 않는 속성을 파악하기 어려웠다. 그 안에 뭐가 사용되는지 뭐가 사용되지 않는지 판단하기 어려워 트리쉐이킹 성능도 좋지 않았다. - 이 모든 과정을 반복해야 되는 HMR 개발서버에서는 캐시를 사용해 변경된 부분만 다시 빌드하는 핫 모듈 리플레이스먼트(HMR) 기능을 제공하는데, 그러나 프로젝트 규모가 커질수록, 위의 작업들을 다시 해야 되는 영역이 늘어나면서, HMR 처리 속도가 느리다..
그래서 Webpack5에서는 ESM기반으로 동작하도록 변경되었지만, 모든 모듈 시스템을 지원하는 Webpack의 특성 때문에 아직도 느린 것은 사실이다.
Vite
Vite는 개발 단계에선 esbuild를, 빌드타임에는 Rollup을 사용한다.
개발환경에선 ESM을 그대로 사용한다.
<script type="module">
을 사용하면 브라우저가 ESM을 그대로 읽을 수 있다. 이를 이용해서 Vite는 개발 단계에서 번들링을 하지 않고, 바로 ESM 모듈을 브라우저에게 준다.
그 ESM 모듈에 import 문이 있으면 → 브라우저가 직접 다른 모듈을 요청한다.
Webpack은 개발서버에서도 의존성 그래프를 만들고 번들링을 하지만, Vite에서는 이렇게 변경된 파일만 브라우저에게 전달하기 때문에 HMR이 매우 빠르다.
위에서 개발서버에는 esbuild가 사용된다고 하는데, 그건 어디서 사용될까?
우선, react나 ts를 브라우저가 모르니까 ESM파일도 트랜스파일을 esbuild가 해준 후에 전달한다.
또, pre-bundling에 쓰인다.
pre-bundling
브라우저가 ESM을 지원하고, 우리도 보통 ESM으로 코드를 작성하지만, 문제가 있다.
- npm 패키지 대부분은 CJS 방식이다.
- lodash, react등 외부 패키지가 import되면 요청 수가 너무 많아진다.
- node_modules 내부 모듈들은 여러 파일들로 쪼개져 있다.
<script type="module">로 건네줘야 할 파일들이 너무나 많다는 것이다.
그래서 Vite는 초기 서버 실행할 때, 한 번만, node_modules를 esbuild로 미리 묶어서 하나의 ESM 파일로 만든다.
그 후엔 다른 패키지를 ESM으로 붙이는 것이다.
빌드단계에선 Rollup을 사용한다.
프로덕션 번들은 여전히 “하나로 묶인 최적화된 파일”이 필요하다.
Vite는 기본적으로 Rollup을 이용하여 빌드를 진행하는데, Rollup은 태생이 ESM 기반이라, 기본적으로 ESM만을 고려하여 번들링을 진행한다. 그래서 동적 require/export.module을 고려하는 Webpack보다 빠르다. 대신 Rollup은 CJS를 사용할거면 플러그인을 추가해야한다.
왜 Vite는 빌드타임에 esbuild를 사용하지 않을까
esbuild는 병렬처리가 가장 적합하다는 Go로 작성되어 있어 속도적으론 압도적 1위다. 물론 Vite에서 esbuild로 빌드할 수 있지만, default는 Rollup이다. 얘가 제일 빠르다고는 하는데 왜 번들러로 esbuild를 사용하지 않는걸까 ?
esbuild가 속도 위주로 만들어진 빌드도구이기 때문이다.
-
Rollup은 esbuild보다 side-Effect 판단을 잘한다.
Rollup은 ESM 전용으로 만들어졌기 때문에,
모든 import/export를 정적(compile-time) 으로 분석하고 사이드 이펙트를 잘 판단한다.- 어떤 export가 실제로 사용되는가
- 어떤 import가 실행만 되고 값이 쓰이지 않는가
- 모듈이 자체 실행 코드를 가지고 있는가 (사이드이펙트)
반면 esbuild는 트리쉐이킹을 지원하지만, 속도를 위해 분석을 단순화했다.
- import/export 관계를 완전히 파악하지 않고,
- 일부 경우에선 “export된 게 안 쓰이면 제거” 정도로만 처리
- default export나 동적 import 내부 부작용은 정확히 판단하지 않음
- 사이드이펙트 판단은 Rollup보다 덜 제거한다.
-
Rollup은 빌드 파이프라인 중간마다 세밀한 개입이 가능하다
Rollup은 플러그인 시스템이 매우 정교하다.
“모듈 로딩 → 파싱 → 변환 → 최적화 → 출력” 모든 단계에 후킹(hook)할 수 있다.예를 들어 Rollup 플러그인은 이런 세밀한 작업이 가능하다.
- 특정 import를 빌드 타임에 치환
- 특정 export만 제거
- 특정 사이드이펙트를 명시적으로 무시
- AST 변환 단계 직접 제어
즉, 플러그인 체인으로 트리쉐이킹이나 번들 최적화를 커스터마이징할 수 있는 구조
이 때문에, esbuild 대신 rollup을 default로 사용한다.
References